package engine.map;

import java.awt.Color;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.PrintStream;
import java.util.Scanner;
import creatures.Creature;
import engine.Game;
import engine.display.Display;
import engine.display.Displayable;
import engine.fov.Board;
import engine.fov.IFovAlgorithm;
import engine.fov.PrecisePermissive;
import engine.map.staticobjects.Floor;
import engine.map.staticobjects.Wall;

/**
 * This is the Map class, used to contain most of the information about the game world.
 * <p>
 * The player and several of the intelligent creatures also have their own maps of the game world.
 * These "memory" maps track information that the player/intelligent creature saw in the past, which
 * may or may not still be accurate. These maps are used in pathfinding by intelligent creatures,
 * and in showing areas the player has explored.
 */
public class Map implements Board, Displayable {
	/**
	 * The data for this map
	 */
	private boolean lit[][];
	private Tile map[][];
	private boolean memory[][];
	private boolean visible[][];

	/**
	 * Creates a new, empty map.
	 * 
	 * @param width
	 *            The width of the map in cells. Valid x-coordinates are from 0 to width-1.
	 * @param height
	 *            The height of the map in cells. Valid y-coordinates are from 0 to height-1.
	 */
	public Map(int width, int height) {
		map = new Tile[width][height];
		for (int x = 0; x < map.length; x++) {
			for (int y = 0; y < map[x].length; y++) {
				map[x][y] = new Floor();
			}
		}
		lit = new boolean[width][height];
		setLit(false);
		memory = new boolean[width][height];
		setRemembered(false);
		visible = new boolean[width][height];
		setVisible(false);
		Game.display.add(this);
	}

	@Override
	public boolean contains(int x, int y) {
		return (x >= 0 && y >= 0 && x < map.length && y < map[0].length);
	}

	/**
	 * @param x
	 *            a x-coordinate in cells
	 * @param y
	 *            a y-coordinate in cells
	 * @return the creature at (x,y) if there is one, otherwise null.
	 */
	public Creature getCreature(int x, int y) {
		for (Creature c : Creature.creatureList) {
			if (x == c.x && y == c.y) {
				return c;
			}
		}
		return null;
	}

	/**
	 * Gets a tile from the map.
	 * 
	 * @param x
	 *            the x-coordinate of the map to get the tile from
	 * @param y
	 *            the y-coordinate of the map to get the tile from
	 * @return the tile, or null if no tile is found
	 */
	public Tile getTile(int x, int y) {
		if (contains(x, y)) {
			return map[x][y];
		} else {
			return null;
		}
	}

	@Override
	public boolean isLit(int x, int y) {
		return contains(x, y) && lit[x][y];
	}

	@Override
	public boolean isObstruction(int x, int y) {
		Tile t = getTile(x, y);
		if (t == null) {
			return true;
		}
		return t.isObstruction();
	}

	/**
	 * @param x
	 *            the x-coordinate in cells
	 * @param y
	 *            the y-coordinate in cells
	 * @return whether the tile at (x,y) is "passable" (can be moved through)
	 */
	public boolean isPassable(int x, int y) {
		Tile t = getTile(x, y);
		if (t == null) {
			return false;
		}
		return t.isPassable();
	}

	/**
	 * @param x
	 *            the x-coordinate in cells
	 * @param y
	 *            the y-coordinate in cells
	 * @return whether x, y is visible
	 */
	public boolean isVisible(int x, int y) {
		return contains(x, y) && visible[x][y];
	}

	/**
	 * Loads the map
	 * 
	 * @param directory
	 *            the directory to load from
	 */
	public void load(String directory) {
		// map already is full of empty tiles... though we will replace them all
		setVisible(false);
		loadStaticObjects(directory);
		// loadDynamicObjects(directory);
	}

	private void loadStaticObjects(String directory) {
		File mapFile = new File(directory + "\\map.txt");
		File memoryFile = new File(directory + "\\memory.txt");
		File colorFile = new File(directory + "\\colors.txt");
		Scanner mapInput = null;
		Scanner colorInput = null;
		Scanner memoryInput = null;
		try {
			mapInput = new Scanner(mapFile);
			colorInput = new Scanner(colorFile);
			memoryInput = new Scanner(memoryFile);
			int y = 0;
			while (mapInput.hasNextLine()) {
				String row = mapInput.nextLine();
				String memRow = memoryInput.nextLine();
				for (int x = 0; x < row.length(); x++) {
					if (row.substring(x, x + 1).equals("#")) {
						// wall
						setTile(x, y, new Wall());
					} else {
						// default tile
						setTile(x, y, new Floor());
					}

					int color = colorInput.nextInt();
					map[x][y].setColor(new Color(color));
					memory[x][y] = memRow.substring(x, x + 1).equals("T");
				}
				y++;
			}

		} catch (FileNotFoundException e) {
			System.err.println("Could not find save file");
		} finally {
			if (mapInput != null) {
				mapInput.close();
			}
			if (colorInput != null) {
				colorInput.close();
			}
		}
	}

	@Override
	public void render() {
		render(Game.display, 0, 0, 0, 0, Game.display.getWidth() - 1, Game.display.getHeight() - 1);

	}

	/**
	 * Draws the data in this map. Use the view parameters to position and size the map on the
	 * screen. Out of bounds cells will not be drawn.
	 * 
	 * @param display
	 *            the display to render upon
	 * @param x
	 *            the x position to draw this at on the screen (in cells)
	 * @param y
	 *            the y position to draw this at on the screen (in cells)
	 * @param x1
	 *            these specify the region of the map that will be drawn
	 * @param y1
	 *            these specify the region of the map that will be drawn
	 * @param x2
	 *            these specify the region of the map that will be drawn
	 * @param y2
	 *            these specify the region of the map that will be drawn
	 */
	public void render(Display display, int x, int y, int x1, int y1, int x2, int y2) {
		// validate the settings
		if (x1 < 0) {
			// shift the draw position to make up for it
			x += -x1;
			// adjust it to 0
			x1 = 0;
		}

		if (y1 < 0) {
			// shift the draw position to make up for it
			y += -y1;
			// adjust it to 0
			y1 = 0;
		}

		if (x2 > map.length) {
			// reduce the width to make up for it
			x2 = map.length;
		}

		if (y2 > map[0].length) {
			// reduce the height to make up for it
			y2 = map[0].length;
		}

		// render each tile
		for (int i = x1; i <= x2; i++) {
			for (int j = y1; j <= y2; j++) {
				if (visible[i][j]) {
					map[i][j].render(display, i, j);
				} else if (memory[i][j]) {
					map[i][j].renderShaded(display, i, j);
				}
			}
		}
	}

	/**
	 * Saves the map to files in the designated folder
	 * 
	 * @param directory
	 *            The directory to save to. Each map should have its own directory.
	 */
	public void save(String directory) {
		saveStaticObjects(directory);
		// saveDynamicOjects(directory);
	}

	private void saveStaticObjects(String directory) {
		PrintStream mapOut = null;
		PrintStream colorOut = null;
		PrintStream memoryOut = null;
		File mapFile = new File(directory + "\\map.txt");
		File colorFile = new File(directory + "\\colors.txt");
		File memoryFile = new File(directory + "\\memory.txt");
		try {
			mapOut = new PrintStream(new FileOutputStream(mapFile));
			colorOut = new PrintStream(new FileOutputStream(colorFile));
			memoryOut = new PrintStream(new FileOutputStream(memoryFile));

			/*
			 * First, output the static object map. This is a highly human readable representation
			 * of all the immovable objects on the map (floors, walls, etc.)
			 */
			for (int y = 0; y < map[0].length; y++) {
				for (int x = 0; x < map.length; x++) {
					// add this static object to the map
					mapOut.append(map[x][y].toString());
					if (memory[x][y]) {
						memoryOut.append("T");
					} else {
						memoryOut.append("F");
					}
					colorOut.append(map[x][y].getColor().getRGB() + " ");
				}

				if (y + 1 != map[0].length) {
					mapOut.append("\r\n");
					memoryOut.append("\r\n");
					colorOut.append("\r\n");
					/*
					 * No trailing newlines (I'm OCD like that)
					 */
				}
			}
		} catch (FileNotFoundException e) {
			e.printStackTrace();
		} finally {
			if (mapOut != null) {
				mapOut.close();
			}
		}
	}

	private void setLit(boolean lit) {
		for (int x = 0; x < this.lit.length; x++) {
			for (int y = 0; y < this.lit[x].length; y++) {
				this.lit[x][y] = lit;
			}
		}
	}

	/**
	 * Flags all tiles as remembered or not. Mainly for debugging.
	 * 
	 * @param remembered
	 *            true to make all tiles remembered, false to make all tiles unexplored
	 */
	public void setRemembered(boolean remembered) {
		for (int x = 0; x < map.length; x++) {
			for (int y = 0; y < map[x].length; y++) {
				memory[x][y] = remembered;
			}
		}
	}

	/**
	 * Replaces an existing tile.
	 * 
	 * @param x
	 *            the x-coordinate of the map to replace
	 * @param y
	 *            the y-coordinate of the map to replace
	 * @param tile
	 *            the tile to replace with
	 * @return success (will fail if (x,y) is outside the map)
	 */
	public boolean setTile(int x, int y, Tile tile) {
		if (contains(x, y)) {
			map[x][y] = tile;
			return true;
		}
		return false;
	}

	/**
	 * Flags all tiles as visible or not. Mainly for debugging.
	 * 
	 * @param visible
	 *            true to make all tiles visible, false to make all tiles hidden
	 */
	public void setVisible(boolean visible) {
		for (int x = 0; x < map.length; x++) {
			for (int y = 0; y < map[x].length; y++) {
				this.visible[x][y] = visible;
			}
		}
	}

	/**
	 * Should be called every time the focal point of this map moves, as this determines the field
	 * of view.
	 * <p>
	 * A very powerful function, this updates the map's view in a region based on a point's field of
	 * view.
	 * 
	 * @param x
	 *            the x-coordinate to calculate the FOV from in cells
	 * @param y
	 *            the y-coordinate to calculate the FOV from in cells
	 * @param radius
	 *            how far the FOV should extend from (x,y)
	 */
	public void updateSight(int x, int y, int radius) {
		setVisible(false);
		IFovAlgorithm p = new PrecisePermissive();
		p.visitFieldOfView(this, x, y, radius);
	}

	@Override
	public void visit(int x, int y) {
		visible[x][y] = true;
		memory[x][y] = true;
	}
}
